package com.fp.m7.client.presenter;

import java.util.ArrayList;
import java.util.List;

import net.customware.gwt.presenter.client.EventBus;
import net.customware.gwt.presenter.client.place.PlaceRequest;
import net.customware.gwt.presenter.client.place.TokenFormatter;
import net.customware.gwt.presenter.client.widget.WidgetDisplay;
import net.customware.gwt.presenter.client.widget.WidgetPresenter;

import com.allen_sauer.gwt.log.client.Log;
import com.fp.m7.client.AccountContext;
import com.fp.m7.client.MyMessages;
import com.fp.m7.client.common.ProfileTab;
import com.fp.m7.client.common.TimeUnit;
import com.fp.m7.client.event.AccountAuthenticatedEvent;
import com.fp.m7.client.event.AccountAuthenticatedEventHandler;
import com.fp.m7.client.event.InfoEvent;
import com.fp.m7.client.event.InfoEvent.UserMessageType;
import com.fp.m7.client.place.ProfilePlace;
import com.fp.m7.client.widget.UserSimpleWidget;
import com.fp.m7.shared.Constants;
import com.fp.m7.shared.dto.AccountDto;
import com.fp.m7.shared.dto.CommentDto;
import com.fp.m7.shared.dto.UserDto;
import com.fp.m7.shared.services.AccountServiceAsync;
import com.fp.m7.shared.services.CommentServiceAsync;
import com.fp.m7.shared.services.FollowServiceAsync;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.HasWidgets;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.inject.Inject;
import com.google.inject.Provider;

/**
 * Profile presenter, the most complicated of all probably. Handles a lot of things: Displaying users comments (blog),
 * his/her followers, users which he/she follows, and some basic info about the user him/herslef. Profile can be viewed
 * by both authorized and not authorized viewers. If one is logged in and is viewing somebody else's profile page, one
 * can follow / unfollow that user in order to see his/her comments in one's timeline view.
 * 
 * @author 7
 */
public class ProfilePresenter extends WidgetPresenter<ProfilePresenter.Display> {

    public interface Display extends WidgetDisplay {

        void setAccountData(AccountDto account);

        HasWidgets getCommentsPanel();

        HasWidgets getFollowersPanel();

        HasWidgets getFollowingPanel();

        HasClickHandlers getPreviousButton();

        HasClickHandlers getNextButton();

        HasClickHandlers getFollowButton();

        HasValue<Boolean> getFollowButtonMode();

        /** null ==> not logged (btn not visible); True ==> Follow visible; False ==> Unfollow visible. */
        void setCanFollow(Boolean mode);

        void focusOnTab(ProfileTab tab);

    }

    private static MyMessages msg = GWT.create(MyMessages.class);

    /**
     * DI container injects a comment presenter provider, so that we can create and attach comment presenters from here
     * directly. Why here? Each comment is displayed according to MVP. So it has the 'presenter' part which handles
     * logic behavior, and it has the 'view' part which delivers the eye-candy. Because there are lots of comments -
     * there ale lots of presenters and their view widgets.
     */
    private Provider<CommentPresenter> commentPresenterProvider;
    private List<CommentPresenter> commentPresenters = new ArrayList<CommentPresenter>(Constants.MAX_COMMENTS_PER_PAGE);

    private TokenFormatter tokenFormatter;
    private AccountContext accountContext;

    private AccountServiceAsync accountService;
    private CommentServiceAsync commentService;
    private FollowServiceAsync followService;

    private AccountDto displayForAccount;

    private String nickname;
    private TimeUnit timeUnit;
    private Integer pageNo;

    private UsersAsyncCallback followersCallback = new UsersAsyncCallback(display.getFollowersPanel());

    private UsersAsyncCallback followingCallback = new UsersAsyncCallback(display.getFollowingPanel());

    private AsyncCallback<List<CommentDto>> commentCallback = new AsyncCallback<List<CommentDto>>() {
        @Override
        public void onSuccess(List<CommentDto> list) {
            clearComments();

            for (CommentDto commentDto : list) {
                CommentPresenter cp = commentPresenterProvider.get();
                cp.setCommentDto(commentDto);
                cp.bind();

                commentPresenters.add(cp);
                display.getCommentsPanel().add(cp.getDisplay().asWidget());
            }
        }

        @Override
        public void onFailure(Throwable throwable) {
            clearComments();
        }
    };

    private AsyncCallback<Boolean> followCallback = new AsyncCallback<Boolean>() {
        @Override
        public void onSuccess(Boolean result) {
            eventBus.fireEvent(new InfoEvent(msg.followSuccess(), UserMessageType.SUCCESS));
            display.getFollowButtonMode().setValue(true);
            fetchFollowers(displayForAccount);
            fetchFollowing(displayForAccount);
        }

        @Override
        public void onFailure(Throwable throwable) {
            Log.info("Following user went wrong", throwable);
        }
    };

    private AsyncCallback<Boolean> unfollowCallback = new AsyncCallback<Boolean>() {
        @Override
        public void onSuccess(Boolean result) {
            eventBus.fireEvent(new InfoEvent(msg.unfollowSuccess(), UserMessageType.SUCCESS));
            display.getFollowButtonMode().setValue(false);
            fetchFollowers(displayForAccount);
            fetchFollowing(displayForAccount);
        }

        @Override
        public void onFailure(Throwable throwable) {
            Log.info("Unfollowing user went wrong", throwable);
        }
    };

    @Inject
    public ProfilePresenter(Display display, EventBus eventBus, TokenFormatter tokenFormatter, //
            AccountContext accountContext, //
            Provider<CommentPresenter> commentPresenterProvider, //
            AccountServiceAsync accountService, //
            CommentServiceAsync commentService, //
            FollowServiceAsync followService) {

        super(display, eventBus);

        this.tokenFormatter = tokenFormatter;
        this.accountContext = accountContext;
        this.commentPresenterProvider = commentPresenterProvider;

        this.accountService = accountService;
        this.commentService = commentService;
        this.followService = followService;
    }

    @Override
    protected void onBind() {
        // react on every account context change
        eventBus.addHandler(AccountAuthenticatedEvent.TYPE, new AccountAuthenticatedEventHandler() {
            @Override
            public void onAccountAuthenticated(AccountAuthenticatedEvent accountAuthenticatedEvent) {
                if (displayForAccount == null) {
                    display.setAccountData(accountAuthenticatedEvent.getAuthenticatedAccount());
                }
            }
        });

        display.getPreviousButton().addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent paramClickEvent) {
                if (pageNo == null) {
                    pageNo = 0;
                } else {
                    if (pageNo > 0) {
                        --pageNo;
                        fetchComments(displayForAccount);
                    }
                }
            }
        });

        display.getNextButton().addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent paramClickEvent) {
                if (pageNo == null) {
                    pageNo = 1;
                } else {
                    ++pageNo;
                }
                fetchComments(displayForAccount);

            }
        });

        display.getFollowButton().addClickHandler(new ClickHandler() {
            @Override
            public void onClick(ClickEvent evt) {
                if (displayForAccount != null) {
                    ToggleButton src = (ToggleButton) evt.getSource();
                    if (src.isDown()) {
                        followService.follow(displayForAccount.getUserDto(), followCallback);
                    } else {
                        followService.unfollow(displayForAccount.getUserDto(), unfollowCallback);
                    }
                }
            }
        });
    }

    @Override
    protected void onUnbind() {
        clearComments();
    }

    @Override
    protected void onRevealDisplay() {
        fetchAccountData();
    }

    private void fetchAccountData() {
        if (nickname == null) {
            displayForAccount(accountContext.getAuthenticatedAccount());
        } else {
            accountService.getDetails(nickname, new AsyncCallback<AccountDto>() {
                @Override
                public void onSuccess(AccountDto accountDto) {
                    displayForAccount(accountDto);
                }

                @Override
                public void onFailure(Throwable throwable) {
                    displayForAccount(null);
                }
            });
        }
    }

    private void displayForAccount(AccountDto accountDto) {
        this.displayForAccount = accountDto;
        display.setAccountData(accountDto);

        if (accountDto == null) {
            eventBus.fireEvent(new InfoEvent(msg.accountNotFound(), UserMessageType.WARNING));
            // clear view:
            clearComments();
            display.getFollowersPanel().clear();
            display.getFollowingPanel().clear();
        } else {
            fetchComments(accountDto);
            fetchFollowers(accountDto);
            fetchFollowing(accountDto);
        }

        AccountDto auth = accountContext.getAuthenticatedAccount();
        if (auth == null) {
            display.setCanFollow(null);
        } else {
            if (auth.equals(accountDto)) {
                // cannot follow self
                display.setCanFollow(null);
            } else {
                boolean isAlreadyFollowing = followingCallback.getCachedResultList().contains(
                        displayForAccount.getUserDto());
                display.setCanFollow(!isAlreadyFollowing);
            }
        }
    }

    private void fetchComments(AccountDto accountDto) {
        assert (accountDto != null);

        int pageNumber = pageNo == null ? 0 : pageNo;
        if (timeUnit == null) {
            commentService.blog(accountDto.getUserDto().getNickname(), pageNumber, commentCallback);
        } else {
            commentService.blog(accountDto.getUserDto().getNickname(), timeUnit.getFrom(pageNumber),
                    timeUnit.getTo(pageNumber), commentCallback);
        }
    }

    private void fetchFollowers(AccountDto accountDto) {
        followService.listFollowers(accountDto.getUserDto(), followersCallback);
    }

    private void fetchFollowing(AccountDto accountDto) {
        followService.listFollowed(accountDto.getUserDto(), followingCallback);
    }

    private void clearComments() {
        display.getCommentsPanel().clear();
        for (CommentPresenter cp : commentPresenters) {
            cp.unbind();
        }
        commentPresenters.clear();
    }

    public String getNickname() {
        return nickname;
    }

    public void setNickname(String nickname) {
        this.nickname = nickname;
    }

    public TimeUnit getTimeUnit() {
        return timeUnit;
    }

    public void setTimeUnit(TimeUnit timeUnit) {
        this.timeUnit = timeUnit;
    }

    public Integer getPageNo() {
        return pageNo;
    }

    public void setPageNo(Integer pageNo) {
        this.pageNo = pageNo;
    }

    public void displayTab(ProfileTab tab) {
        display.focusOnTab(tab);
    }

    /**
     * In the case of ProfilePresenter, two almost identical callback are needed. Both are used to populate (or clear)
     * the contents of some abstract panel with widgets. So this is a class to define the uniform behavior, allowing to
     * set the contents panel on which to operate.
     */
    private class UsersAsyncCallback implements AsyncCallback<List<UserDto>> {

        private HasWidgets contentPanel;
        private List<UserDto> cache = new ArrayList<UserDto>(0);

        public UsersAsyncCallback(HasWidgets contentPanel) {
            this.contentPanel = contentPanel;
        }

        @Override
        public void onSuccess(List<UserDto> list) {
            cache = list;
            contentPanel.clear();

            for (UserDto userDto : list) {
                PlaceRequest req = new PlaceRequest(ProfilePlace.NAME) //
                        .with(ProfilePlace.NICKNAME, userDto.getNickname());

                UserSimpleWidget userSimpleWidget = new UserSimpleWidget();
                userSimpleWidget.setContents(userDto.getNickname(), tokenFormatter.toHistoryToken(req),
                        userDto.getAvatarUrl());

                contentPanel.add(userSimpleWidget);
            }
        }

        @Override
        public void onFailure(Throwable throwable) {
            contentPanel.clear();
        }

        public List<UserDto> getCachedResultList() {
            return cache;
        }
    }
}
